/**
 * This file Copyright (c) 2003-2009 Magnolia International
 * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
 *
 *
 * This file is dual-licensed under both the Magnolia
 * Network Agreement and the GNU General Public License.
 * You may elect to use one or the other of these licenses.
 *
 * This file is distributed in the hope that it will be
 * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
 * Redistribution, except as permitted by whichever of the GPL
 * or MNA you select, is prohibited.
 *
 * 1. For the GPL license (GPL), you can redistribute and/or
 * modify this file under the terms of the GNU General
 * Public License, Version 3, as published by the Free Software
 * Foundation.  You should have received a copy of the GNU
 * General Public License, Version 3 along with this program;
 * if not, write to the Free Software Foundation, Inc., 51
 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * 2. For the Magnolia Network Agreement (MNA), this file
 * and the accompanying materials are made available under the
 * terms of the MNA which accompanies this distribution, and
 * is available at http://www.magnolia-cms.com/mna.html
 *
 * Any modifications to this file must keep this entire header
 * intact.
 *
 */
package com.google.code.mgnlgroovy.scheduler;

import info.magnolia.module.ModuleLifecycle;
import info.magnolia.module.ModuleLifecycleContext;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

import org.apache.commons.lang.StringUtils;
import org.quartz.CronTrigger;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Scheduler module. Under the node jobs one can create jobs by defining the command to use. The scheduling is done
 * based on a cron syntax scheduling expression. The job definition can contain a params node which can hold values
 * passed to the command in the execution-context.
 * @author philipp
 * @version $Id: SchedulerModule.java 3 2009-04-16 22:48:19Z federico.grilli $
 */
public class SchedulerModule implements ModuleLifecycle
{

    /**
     * Logger.
     */
    protected static final Logger log = LoggerFactory.getLogger(SchedulerModule.class);

    private static SchedulerModule instance;

    private List<JobDefinition> jobs = new ArrayList<JobDefinition>();

    private boolean running = false;

    /**
     * The quartz scheduler
     */
    private static Scheduler scheduler;

    public SchedulerModule()
    {
        instance = this;
    }

    public List<JobDefinition> getJobs()
    {
        return this.jobs;
    }

    public void setJobs(List<JobDefinition> jobs)
    {
        log.debug("setting jobs...");
        this.jobs = jobs;
    }

    public void addJob(JobDefinition job) throws SchedulerException
    {
        log.debug("adding job {}", job.getName());
        jobs.add(job);
        if (running)
        {
            initJob(job);
        }
    }

    /**
     * Stops the scheduler
     */
    public void stop(ModuleLifecycleContext moduleLifecycleContext)
    {
        try
        {
            if (moduleLifecycleContext.getPhase() == ModuleLifecycleContext.PHASE_SYSTEM_SHUTDOWN)
            {
                scheduler.shutdown();
            }
            running = false;
        }
        catch (SchedulerException e)
        {
            log.error("Can't stop scheduler properly", e);
        }
    }

    /**
     * Start scheduler and add jobs
     */
    public void start(ModuleLifecycleContext moduleLifecycleContext)
    {
        try
        {
            if (moduleLifecycleContext.getPhase() == ModuleLifecycleContext.PHASE_SYSTEM_STARTUP)
            {
                initScheduler();
            }
            running = true;
            initJobs();
        }
        catch (SchedulerException e)
        {
            log.error("Can't start scheduler due to the following error {}", e);
            return;
        }
    }

    /**
     * Add all the jobs defined in the jobs node
     */
    protected void initJobs()
    {
        for (JobDefinition job : jobs)
        {
            try
            {
                initJob(job);
            }
            catch (SchedulerException e)
            {
                log.error("Can't initialize job {}", e.getMessage());
            }
        }
    }

    /**
     * Initialize a singel job
     */
    protected void initJob(JobDefinition job) throws SchedulerException
    {
        if (job.isActive())
        {
            try
            {
                stopJob(job.getName());
                startJob(job);
            }
            catch (SchedulerException e)
            {
                throw new SchedulerException("Can't schedule job" + job.getName(), e);
            }
        }
        else
        {
            try
            {
                stopJob(job.getName());
            }
            catch (SchedulerException e)
            {
                throw new SchedulerException("Can't delete inactive job " + job.getName(), e);
            }
        }
    }

    protected void startJob(JobDefinition job) throws SchedulerException
    {
        Trigger trigger = null;
        try
        {
            String cron = cronToQuarzCron(job.getCron());

            trigger = new CronTrigger(job.getName(), SchedulerConsts.SCHEDULER_GROUP_NAME, cron);

            Calendar startTime = Calendar.getInstance();
            Calendar endTime = Calendar.getInstance();

            if (job.getStartTime() != 0 && job.getStartTime() >= trigger.getStartTime().getTime())
            {
                startTime.setTimeInMillis(job.getStartTime());
                trigger.setStartTime(startTime.getTime());
            }
            else
            {
                startTime.setTime(trigger.getStartTime());
                job.setStartTime(startTime.getTimeInMillis());
            }

            if (job.getEndTime() != 0)
            {
                endTime.setTimeInMillis(job.getEndTime());
                trigger.setEndTime(endTime.getTime());
            }

        }
        catch (ParseException e)
        {
            log.error("Can't parse the job's cron expression [" + job.getCron() + "]", e);
            return;
        }

        JobDetail jd = null;
        if (StringUtils.isNotEmpty(job.getGroovyScript()))
        {
            jd = new JobDetail(job.getName(), SchedulerConsts.SCHEDULER_GROUP_NAME, GroovyJob.class);
            jd.getJobDataMap().put(SchedulerConsts.CONFIG_JOB_GROOVY, job.getGroovyScript());
        }
        else
        {
            jd = new JobDetail(job.getName(), SchedulerConsts.SCHEDULER_GROUP_NAME, CommandJob.class);
            jd.getJobDataMap().put(SchedulerConsts.CONFIG_JOB_COMMAND, job.getCommand());
            jd.getJobDataMap().put(SchedulerConsts.CONFIG_JOB_COMMAND_CATALOG, job.getCatalog());
        }

        jd.getJobDataMap().put(SchedulerConsts.CONFIG_JOB_PARAMS, job.getParams());
        scheduler.scheduleJob(jd, trigger);
        log.info("Job {} added [{}]. Will fire next time at {}", new Object[]{job.getName(), job.getCron(), trigger.getNextFireTime()});
    }

    protected String cronToQuarzCron(String cron)
    {
        // quarz does not support * for both day of week and day of month. we simple handle this
        String[] tokens = StringUtils.split(cron);
        if (tokens.length >= 6)
        {
            if (!tokens[3].equals("?") && !tokens[5].equals("?"))
            {
                if (tokens[5].equals("*"))
                {
                    tokens[5] = "?";
                }
                else if (tokens[3].equals("*"))
                {
                    tokens[3] = "?";
                }
            }
        }
        cron = StringUtils.join(tokens, " ");
        return cron;
    }

    /**
     * Deletes savely the job. If the job is not defined the metho does NOT throw an execption
     * @param name
     * @throws SchedulerException
     */
    protected void stopJob(String name) throws SchedulerException
    {
        scheduler.deleteJob(name, SchedulerConsts.SCHEDULER_GROUP_NAME);
    }

    /**
     * Start the scheduler. No special configuration done yet.
     * @throws SchedulerException
     */
    private static void initScheduler() throws SchedulerException
    {
        SchedulerFactory sf = new StdSchedulerFactory();
        scheduler = sf.getScheduler();
        scheduler.start();
    }

    /**
     * Singleton instance of the module
     * @return
     */
    public static SchedulerModule getInstance()
    {
        return instance;
    }

    private static ThreadLocal<Scheduler> schedulerHolder = new ThreadLocal<Scheduler>()
    {
        @Override
        protected Scheduler initialValue()
        {
            if (scheduler == null)
            {
                try
                {
                    initScheduler();
                }
                catch (SchedulerException e)
                {
                    log.error(e.getMessage());
                    throw new RuntimeException(e);
                }
            }
            return scheduler;
        }
    };

    public static Scheduler getScheduler()
    {
        return schedulerHolder.get();
    }
}